#!/usr/bin/python
# vim: set fileencoding=utf-8 :
# Copyright (C) 2007 Kévin Dunglas <dunglas@gmail.com>
#
# Authors:
#  Kévin Dunglas
#
# This program is a part of a the Google Summer Of Code 2007
# For futher information see :
# http://code.google.com/soc/ubuntu/appinfo.html?csaid=EF4FCF874D88234
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License as published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE.  See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Software Foundation, Inc., 59 Temple
# Place, Suite 330, Boston, MA 02111-1307 USA

import os
import sys
import pygtk
pygtk.require('2.0')
import gobject
import gtk
import gtk.glade
from time import sleep
import threading
import gettext
import locale
import conceal
import gnomekeyring

import __builtin__
__builtin__._ = gettext.gettext

APPNAME="conceal-gtk"
APPVERSION="0.0.5"
CACHE = os.environ['HOME'] + "/.conceal/cache"
GLADE = "/usr/share/conceal/conceal.glade"
ICON = "/usr/share/pixmaps/conceal.png"
class Keyring:
    """Store passwords using GNOME Keyring if available"""
    
    # No documentation available for the Python binding of GNOME Keyring
    # Inspirated from the the following Gajim code
    # http://trac.gajim.org/browser/trunk/src/common/passwords.py
    
    def __init__(self, keyring):
        self.available = False
        self.keyring = keyring # Must be "default" or "session"
        
        # Test if the gnomekeyring module is installed
        try:
            import gnomekeyring
        except ImportError:
            self.available = False
            return

        # Test if GNOME Keyring is available
        if gnomekeyring.is_available():
            self.available = True
        else:
            return
        
        # Sync with the default keyring
        try:
            gnomekeyring.create_sync(self.keyring, None)
        except gnomekeyring.AlreadyExistsError:
            pass
    
    def save_password(self, folder, password):
        """Save a password for an encrypted folder identified by his digest"""
        
        if not self.available:
            return None
        
        attributes = {"path":folder.path, "conceal":True}
        
        try:
        # Add an item
            auth = gnomekeyring.item_create_sync(
                self.keyring, # Keyring
                gnomekeyring.ITEM_GENERIC_SECRET, # Item type
                folder.digest, # Item name
                attributes, # Attributes
                password, # Password
                False) # Update if already exists
        except gnomekeyring.NoKeyringDaemonError:
            self.available = False
            return None
        else:
            folder.attributes["gnomekeyring_id"] = int(auth)
            folders.update(folder)
    
    def get_password(self, folder):
        """Get the password of an encrypted folder, return None if not stored"""
        
        if not self.available:
            return None
        
        try:
            return gnomekeyring.item_get_info_sync(self.keyring,
                folder.attributes["gnomekeyring_id"]).get_secret()
        except gnomekeyring.DeniedError:
            return None
        except gnomekeyring.NoKeyringDaemonError:
            self.available = False
            return None

    def delete_password(self, folder):
        """Remove a stored password"""
        
        gnomekeyring.item_delete_sync(self.keyring,\
            folder.attributes["gnomekeyring_id"])
        
        folder.attributes.pop("gnomekeyring_id")
    
    def change_password(self, folder, new_password):
        """Change a stored password"""
        
        self.delete_password(folder.attributes["gnomekeyring_id"])
        return self.save_password(folder, new_password)

 
class GladeHandlers:
    """Connectors to GTK/Glade events"""
    def gtk_main_quit(event):
        quit()

    def on_manager_add_clicked(event):
        windows["manager"]["manager"].set_sensitive(False)
        Crypt()
    
    def on_manager_delete(envet, state):
        exit()

    def on_crypt_apply_clicked(event):
        path = windows["crypt"]["crypt_folder"].get_filename()
        password = windows["crypt"]["crypt_password"].get_text()
        confirmation = windows["crypt"]["crypt_confirmation"].get_text()
        if Util().valid_crypt(path, password, confirmation):
            Encrypt(path, password)
            #WidgetsWrapper.hide_or_quit(widgets, "crypt")

    def on_crypt_destroy(event):
        windows.hide_or_quit("crypt", 1)

    def on_crypt_cancel_clicked(event):
        windows.hide_or_quit("crypt", 1)
    
    def on_open_destroy(event):
        windows.hide_or_quit("open", 1)

    def on_open_cancel_clicked(event):
        windows.hide_or_quit("open", 1)
    
    def on_open_apply_clicked(event):
        if windows["open"]["open_autoclose"].get_active():
            idle = windows["open"]["open_time"].get_value_as_int()
        else:
            idle = None
        
        if windows["open"]["open_savepass"].get_active():
            i = windows["open"]["open_savechoice"].get_active()
            if i == 0:
                keyring = "session"
            else:
                keyring = "default"
        else:
            keyring = None
        
        Mount(windows["open"]["open_path"].get_text(),\
            windows["open"]["open_password"].get_text(), idle, keyring)
    
    def on_decrypt_destroy(event):
        windows.hide_or_quit("decrypt", 1)
    
    def on_decrypt_cancel_clicked(event):
        windows.hide_or_quit("decrypt", 1)
    
    def on_open_autoclose_toggled(event):
        if windows["open"]["open_autoclose"].get_active():
            windows["open"]["open_time"].set_sensitive(True)
        else:
             windows["open"]["open_time"].set_sensitive(False)
    
    def on_open_savepass_toggled(event):
        if windows["open"]["open_savepass"].get_active():
            windows["open"]["open_savechoice"].set_sensitive(True)
        else:
            windows["open"]["open_savechoice"].set_sensitive(False)
    
    def on_decrypt_apply_clicked(event):
        windows["decrypt"]["decrypt"].set_sensitive(False)
        Sdecrypt(windows["decrypt"]["decrypt_path"].get_text(),\
            windows["decrypt"]["decrypt_password"].get_text())

    def on_error_close_clicked(event):
        windows.hide_or_quit("error", 1)
    
    def on_error_delete_event(event, state):
        windows.hide_or_quit("error", 1)
    
    def on_manager_list_cursor_changed(event):
        (m, i) = windows["manager"]["manager_list"].get_selection().\
            get_selected()
        if m.get_value(i, 2) == _("Yes"):
            windows["manager"]["manager_open_close"].set_label("gtk-close")
        else:
            windows["manager"]["manager_open_close"].set_label("gtk-open")
        windows["manager"]["manager_open_close"].set_sensitive(True)
        windows["manager"]["manager_decrypt"].set_sensitive(True)
        windows["manager"]["manager_properties"].set_sensitive(True)
    
    def on_manager_list_drag_data_received(\
        event, b, c, d, e, f, g):
        data = e.data.split("file://")[1].split("\r\n")[0]
        if os.path.isdir(data):
            Crypt(data)
        else:
            Util().error_box(_("You must drag a folder."))
    
    def on_manager_open_close_clicked(event):
        (m, i) = windows["manager"]["manager_list"].get_selection().\
            get_selected()
        path = m.get_value(i, 1)
        if m.get_value(i, 2) == _("Yes"): 
            windows["manager"]["manager_open_close"].set_sensitive(False)
            windows["manager"]["manager_properties"].set_sensitive(False)
            windows["manager"]["manager_decrypt"].set_sensitive(False)
            Unmount(path)
        else:
            windows["manager"]["manager"].set_sensitive(False)
            Open(path)
    
    def on_manager_properties_clicked(event):
        (m, i) = windows["manager"]["manager_list"].get_selection().get_selected()
        path = m.get_value(i, 1)
        windows.load("properties")
        windows["properties"]["properties_path"].set_text(path)
        windows["manager"]["manager"].set_sensitive(False)
        
    def on_manager_decrypt_clicked(event):
        windows["manager"]["manager"].set_sensitive(False)
        (m, i) = windows["manager"]["manager_list"].get_selection().\
            get_selected()
        path = m.get_value(i, 1)
        Decrypt(path)

    def on_manager_close_clicked(event):
        exit()
    
    def on_manager_destroy(event):
        exit()

    def on_properties_cancel_clicked(event):
        windows.hide_or_quit("properties")
    
    def on_properties_apply_clicked(event):
        old = windows["properties"]["properties_old"].get_text()
        new = windows["properties"]["properties_new"].get_text()
        confirmation = windows["properties"]["properties_confirmation"].\
            get_text()
        if Util().valid_properties(new, confirmation):
            ChangePassword(windows["properties"]["properties_path"].get_text(),\
                old, new)
    
    def on_properties_delete_event(event, state):
        windows.hide_or_quit("properties")


class Windows:
    """Wrapper for GTK/Glade windows"""
    def __init__(self):
        self.win = {}
    
    def load(self, name):
        self.win[name] = Widgets(name)
        if not name == "manager":
            try:
                windows[name][name].set_transient_for(\
                    windows["manager"]["manager"])
            except:
                pass
        
    def __getitem__(self, key):
        """Alow to use windows['window']['widget']"""
        return self.win[key]

    def hide_or_quit(self, name, para=0):
        """Hide the window if manager is still active, either quit"""
        try:
            manager.update()
            windows["manager"]["manager_open_close"].set_sensitive(False)
            windows["manager"]["manager_properties"].set_sensitive(False)
            windows["manager"]["manager_decrypt"].set_sensitive(False)
            windows["manager"]["manager"].set_sensitive(True)
            try:
                windows[name][name].destroy()
            except:
                pass
        except:
            quit(para)


class Widgets:
    """Wrapper for GTK/Glade widgets"""
    def __init__(self, window):
        """Display a window"""
        self.widgets = gtk.glade.XML(GLADE, window)
        self.widgets.signal_autoconnect(GladeHandlers.__dict__)

    def __getitem__(self, key):
        """Allow to use windows['windows']['widget'].action()"""
        return self.widgets.get_widget(key)


class Util:
    """Utilities"""
    def error_box(self, msg):
        """Display an error box"""
        windows.load("error")
        windows["error"]["error_label"].set_markup("<span weight=\"bold\">" +\
            msg + "</span>")
    
    def warning_box(self, msg):
        """Display a warning box"""
        windows.load("warning")
        windows["warning"]["warning_label"].set_markup("<span weight=\"bold\">"\
            + msg + "</span>")

    def valid_crypt(self, path, password, confirmation):
        """Validation for the crypt window"""
        
        if not os.path.exists(path):
            self.error_box(_("You must select a valid folder."))
            return False
        if len(password) == 0:
            self.error_box(_("Password can not be null."))
            return False
        if not password == confirmation:
            self.error_box(_("Password do not match with the confirmation."))
            return False
        return True
    
    def valid_properties(self, new, confirmation):
        """Validation for the crypt window"""
        if len(new) == 0:
            self.error_box(_("Password can not be null."))
            return False
        if not new == confirmation:
            self.error_box(
                _("New password do not match with the confirmation."))
            return False
        return True


class Pulser(threading.Thread):
    """Pulse the progressbar"""
    
    stopthread = threading.Event()
    
    def run(self):
        while not self.stopthread.isSet() :
            gtk.gdk.threads_enter()
            windows["progressbar"]["progressbar_progressbar"].pulse()
            gtk.gdk.threads_leave()
            sleep(0.1)

    def stop(self):
        """Stop method, sets the event to terminate the thread's main loop"""
        self.stopthread.set()


class Progressbar:
    """Display a pulsing progressbar"""
    def __init__(self, msg=None):
        self.msg = msg

    def start(self):
        """Start the progress bar"""
        windows.load("progressbar")
        if self.msg is not None:
            windows["progressbar"]["progressbar"].set_title(self.msg)
            windows["progressbar"]["progressbar_progressbar"].set_text(self.msg)
        self.pulser = Pulser()
        self.pulser.start()

    def stop(self):
        """Stop the progressbar"""
        self.pulser.stop()
        windows["progressbar"]["progressbar"].destroy()
    

class Manager:
    """Main window"""
    def __init__(self):
        """Launch the Cypted Folder manager"""
        windows.load("manager")
        
        column0 = gtk.TreeViewColumn(None, gtk.CellRendererPixbuf(), pixbuf=0)
        column0.set_resizable(False)
        column0.set_sort_column_id(0)
        
        
        column1 = gtk.TreeViewColumn(_("Folder"), gtk.CellRendererText(),\
            text=1)
        column1.set_resizable(True)
        column1.set_sort_column_id(1)
                
        column2 = gtk.TreeViewColumn(_("Is open"), gtk.CellRendererText(), text=2)
        column2.set_resizable(True)
        column2.set_sort_column_id(2)
        
        windows["manager"]["manager_list"].append_column(column0)
        windows["manager"]["manager_list"].append_column(column1)
        windows["manager"]["manager_list"].append_column(column2)
        
        self.folderList = gtk.ListStore(gtk.gdk.Pixbuf, str, str)
        windows["manager"]["manager_list"].set_model(self.folderList)
        
        target = [('text/uri-list', 0, 0)]
        windows["manager"]["manager_list"].drag_dest_set(gtk.DEST_DEFAULT_ALL,\
            target, gtk.gdk.ACTION_COPY)
        self.update()

    def update(self):
        """Update the tree view with the folders list"""
        self.folderList.clear()
        for f in folders.li:
            #path = f.path.split("/")
            #path = path[len(path) - 1]
            if f.opened == True:
                o = _("Yes")
            else:
                o = _("No")
            image = gtk.gdk.pixbuf_new_from_file(ICON)
            self.folderList.append([image, f.path, o])


class Crypt:
    """Encrypted folder chooser"""
    def __init__(self, path=None):
        windows.load("crypt")
        if path is not None and os.path.exists(path):
            path = conceal.Util().fullpath(path)
            windows["crypt"]["crypt_folder"].set_current_folder(path)
            windows["crypt"]["crypt_folder"].set_sensitive(False)


class Rencrypt(threading.Thread):
    """Encrypt while displaying a progressbar"""
    #def setFolder(self, folder):
    #    self.folder = folder
    
    def set_para(self, folder, password):
        """Set what folder and password to use"""
        self.folder = folder
        self.password = password

    def run(self):
        """Main"""
        try:
            conceal.Manage(self.folder).crypt(self.password)
        except conceal.AlreadyEncrypted:
            progressbar.stop()
            windows["crypt"]["crypt"].set_sensitive(True)
            Util().error_box(_("%s is already an encrypted folder.")
                % self.folder.path)
#        except conceal.NotWritable:
#            windows["crypt"]["crypt"].set_sensitive(True)
#            Util().error_box(
#                _("%s and all his files must be writable.") % self.folder.path)
        else:
            folders.add(self.folder)
            progressbar.stop()
            windows["crypt"]["crypt"].set_sensitive(True)
            windows.hide_or_quit("crypt")


class Encrypt:
    """Threads initiation for encrypting"""
    def __init__(self, path, password):
        global progressbar
        folder = conceal.Folder(path)
        progressbar = Progressbar()
        r = Rencrypt()
        r.set_para(folder, password)
        windows["crypt"]["crypt"].set_sensitive(False)
        progressbar.start()
        r.start()


class Open:
    """Open an encrypted folder"""
    def __init__(self, path):
        try:
            folder = folders.get(path)
        except conceal.NoEncrypted:
            Util().error_box(_("%s is not an encrypted folder.") % path)
            return
        
        # Try to get password in default keyring
        global current_keyring
        password = None
        current_keyring = "default"
        k = Keyring(current_keyring)
        # Test if gnomekeyring is available
        if k.available:
            # Test if a "gnomekeyring_id" attribute is set
            try:
                # Get the gnomekeyring id
                auth = folder.attributes["gnomekeyring_id"]
            except:
                current_keyring = None
            else:
                # Get the password
                password = k.get_password(folder)
                # If no password, try in the session keyring
                if password == None:
                    current_keyring = "session"
                    k = Keyring(current_keyring)
                    password = k.get_password(folder)
                else:
                    current_keyring = None
        #if password == None:
        #    # If the password is always Null, prompt for the password
        #    windows.load("open")
        #    windows["open"]["open_path"].set_text(path)
        #else:
        #    Mount(path, password)
        windows.load("open")
        windows["open"]["open_path"].set_text(path)
        if not password == None:
            windows["open"]["open_password"].set_text(password)
            windows["open"]["open_savepass"].set_active(True)

        if current_keyring == "default":
            windows["open"]["open_savechoice"].set_active(1)
        else:
            windows["open"]["open_savechoice"].set_active(0)
        
        
class Decrypt:
    """Decrypt window"""
    def __init__(self, path):
        try:
            folder = folders.get(path)
        except:
            Util().error_box("This is not an encrypted folder")
        else:
            windows.load("decrypt")
            windows["decrypt"]["decrypt_password"].set_text("")
            windows["decrypt"]["decrypt_path"].set_text(path)


class Sdecrypt:
    """Decryption initiation"""
    def __init__(self, path, password):
        global progressbar
        progressbar = Progressbar()
        r = Rdecrypt()
        r.set_para(path, password)
        progressbar.start()
        r.start()


class Rdecrypt(threading.Thread):
    """Real decryption"""  
    
    def set_para(self, path, password):
        """Set what path and password to use"""
        self.path = path
        self.password = password

    def run(self):
        """Main"""
        try:
            folder = folders.get(self.path)
        except conceal.NoEncrypted:
            progressbar.stop()
            Util().error_box(_("%s is not an encrypted folder.") % self.path)
            windows["decrypt"]["decrypt"].destroy()
            return
        try:
            conceal.Manage(folder).decrypt(self.password)
        except conceal.BadPassword:
            progressbar.stop()
            Util().error_box(_("Your password is wrong."))
            windows["decrypt"]["decrypt"].set_sensitive(True)
        else:
            folders.rem(folder)
            progressbar.stop()
            windows.hide_or_quit("decrypt")


class Mount:
    """Mount an encrypted folder"""
    def __init__(self, path, password, idle=None, keyring=None):
        try:
            folder = folders.get(path)
        except conceal.NoEncrypted:
            Util().error_box(_("%s is not a crypted folder.") % folder.path)
            return
        try:
            folder = conceal.Manage(folder).mount(password, idle)
        except conceal.AlreadyOpened:
            Util().error_box(_("%s is already open.") % folder.path)
        except conceal.BadPassword:
            Util().error_box(_("Your password is wrong."))
        else:
            global current_keyring
            if not current_keyring == None:
                if keyring == None:
                    k = Keyring(current_keyring)
                    k.delete_password(folder)
                elif not current_keyring == keyring:
                     k = Keyring(current_keyring)
                     k.delete_password(folder)
                     k = Keyring(keyring)
                     k.save_password(folder, password)
            elif not keyring == None:
                k = Keyring(keyring)
                k.save_password(folder, password)
            windows.hide_or_quit("open")


class Unmount:
    """Unmount an encrypted folder"""
    def __init__(self, path):
        try:
            folder = folders.get(path)
        except conceal.NoEncrypted:
            Util().error_box(_("%s is not an encrypted folder.") % path)
            return
        try:
            folder = conceal.Manage(folder).unmount()
        except conceal.NotOpened:
            Util().error_box(_("%s is not opened.") % folder.path)
        else:
            folders.update(folder)
            try:
                manager.update()
            except:
                exit()


class ChangePassword:
    """Change a folder password"""
    def __init__(self, path, old, new):
        try:
            folder = folders.get(path)
        except conceal.NoEncrypted:
            Util().error_box(_("%s is not a crypted folder.") % folder.path)
        try:
            folder = conceal.Manage(folder).change_password(old, new)
        except conceal.BadPassword:
            Util().error_box(_("Bad password."))
        else:
            # Try to get password in default keyring
            global current_keyring
            current_keyring = "default"
            k = Keyring(current_keyring)
            # Test if gnomekeyring is available
            if k.available:
                # Test if a "gnomekeyring_id" attribute is set
                try:
                    # Get the gnomekeyring id
                    auth = folder.attributes["gnomekeyring_id"]
                except:
                    pass
                else:
                    # Get the password
                    password = k.get_password(folder)
                    # If no password, try in the session keyring
                    if password == None:
                        current_keyring = "session"
                        k = Keyring(current_keyring)
                        password = k.get_password(auth)
                    if not password == None:
                         k.change_password(folder, new)
                        
            windows.hide_or_quit("properties")

def quit(para=0):
    """Quit the program"""
    if loop:
        gtk.main_quit()
    exit(para)


def exit(para=0):
    data.save()
    sys.exit(para)


def main():
    global data, folders, manager, windows, loop
    loop = False
    data = conceal.Data()
    folders = data.folders
    folders.clean()
    gtk.gdk.threads_init()
    windows = Windows()
    # Command Line Interface
    if len(sys.argv) == 1:
        manager = Manager()
        
    elif len(sys.argv) == 2 and (sys.argv[1] == "--info" or sys.argv[1] == "-i"):
        msg = _("""%s, an encrypted folder manager
Options:
--version								show program's version number and exit
-i, --info								show this help message and exit
-e [FOLDER], --encrypt [FOLDER]	encrypt folder
-o FOLDER, --open folder			open an encrypted folder
-c FOLDER, --close folder			close an encrypted folder""") % APPNAME
        exit(msg)
        
    elif len(sys.argv) == 2 and sys.argv[1] == "--version":
        msg = "%s %s" % (APPNAME, APPVERSION)
        exit(msg)
    
    elif len(sys.argv) == 2 and sys.argv[1] == "--clean":
        folders.clean()
        exit()

    elif len(sys.argv) >= 2 and len(sys.argv) <= 3 and\
        (sys.argv[1] == "--encrypt" or sys.argv[1] == "-e"):
        if len(sys.argv) == 3:
            c = Crypt(sys.argv[2])
        else:
            c = Crypt()

    elif len(sys.argv) == 3 and (sys.argv[1] == "--close" or\
        sys.argv[1] == "-c"):
        Unmount(sys.argv[2])

    elif len(sys.argv) == 3 and (sys.argv[1] == "--open" or\
        sys.argv[1] == "-o"):
        Open(sys.argv[2])

    else:
        msg = """Invalid option
Try « %s --info » to get more information""" % APPNAME
        Util().error_box(msg)
    
    loop = True
    gtk.main()
    exit()

if __name__ == '__main__':
    main()
